﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Sharebert.BootstrapServer.API;
using Sharebert.LinkLayerLibrary;
using Sharebert.LinkLayerLibrary.API;
using Sharebert.LoggingLibrary.API;

namespace Sharebert.BootstrapServer
{
    /// <summary>
    /// Servers network interface used to serve clients. Can be started and stopped.
    /// Triggers events if a client pings or requests a list of peers.
    /// </summary>
    public class BootstrapServer : IBootstrapServer
    {

        private const int DEFAULT_TIMEOUT = 30;

        private const int DEFAULT_BACKLOG = 10;


        public BootstrapServer(IPAddress listenAddress, int listenPort, ILogger logger)
        {
            if (listenPort < IPEndPoint.MinPort || listenPort > IPEndPoint.MaxPort)
            {
                throw new ArgumentOutOfRangeException("No valid port!");
            }
            this.activeConnections = new ThreadSafeList<IConnection>();
            this.IP = listenAddress;
            this.Port = listenPort;
            this.Logger = logger;
            this.Timeout = DEFAULT_TIMEOUT;
            this.AllowedClients = DEFAULT_BACKLOG;
        }

        public BootstrapServer(string listenIP, int listenPort, ILogger logger)
        {
            IPAddress ip = null;
            if (!IPAddress.TryParse(listenIP, out ip))
            {
                throw new ArgumentException("No valid IP address!");
            }
            if (listenPort < IPEndPoint.MinPort || listenPort > IPEndPoint.MaxPort)
            {
                throw new ArgumentOutOfRangeException("No valid port!");
            }

            this.activeConnections = new ThreadSafeList<IConnection>();
            this.IP = ip;
            this.Port = listenPort;
            this.Logger = logger;
            this.Timeout = DEFAULT_TIMEOUT;
            this.AllowedClients = DEFAULT_BACKLOG;
        }

        /// <summary>
        /// Opens a network socket and waits for new connections.
        /// </summary>
        /// <exception cref="NetworkException">Server has already been started</exception>
        public void Start()
        {
            if (this.listenConnection != null && this.listenConnection.Connected)
            {
                throw new NetworkException("Already started!");
            }

            this.listenConnection = new Connection();
            this.listenConnection.ReadTimeout = this.Timeout;
            this.listenConnection.NewConnectionEstablished += new EventHandler<ConnectionEventArgs>(HandleNewConnection);

            this.Logger.Debug("Start listening...");
            this.listenConnection.Listen(this.IP, this.Port, this.AllowedClients);
        }

        /// <summary>
        /// Stops listening and closes the socket.
        /// </summary>
        public void Stop()
        {
            this.listenConnection.Close();
            foreach (IConnection conn in this.activeConnections)
            {
                conn.Close();
            }
        }

        /// <summary>
        /// Handles new connection docked on the listening socket.
        /// Adds the new connection to connection list and and registers events.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e">The new connection</param>
        private void HandleNewConnection(object sender, ConnectionEventArgs e)
        {
            e.Connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(ProcessMessage);
            e.Connection.ConnectionTimedOut += new EventHandler<EventArgs>(ProcessConnectionTimedOut);
            e.Connection.ConnectionClosed += new EventHandler<EventArgs>(ProcessConnectionClosed);
            this.activeConnections.Add(e.Connection);
        }

        /// <summary>
        /// A client connection has timed out.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessConnectionTimedOut(object sender, EventArgs e)
        {
            IConnection conn = sender as IConnection;
            conn.MessageReceived -= new EventHandler<MessageReceivedEventArgs>(ProcessMessage);
            conn.ConnectionTimedOut -= new EventHandler<EventArgs>(ProcessConnectionTimedOut);
            conn.ConnectionClosed -= new EventHandler<EventArgs>(ProcessConnectionClosed);
            this.Logger.Info(String.Format("Connection to {0}:{1} timed out. Local end point was {2}:{3}",
                                            conn.RemoteAddress, conn.RemotePort, conn.LocalAddress, conn.LocalPort));
            this.activeConnections.Remove(conn);
        }

        /// <summary>
        /// A client connection has been closed.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessConnectionClosed(object sender, EventArgs e)
        {
            IConnection conn = sender as IConnection;
            conn.MessageReceived -= new EventHandler<MessageReceivedEventArgs>(ProcessMessage);
            conn.ConnectionTimedOut -= new EventHandler<EventArgs>(ProcessConnectionTimedOut);
            conn.ConnectionClosed -= new EventHandler<EventArgs>(ProcessConnectionClosed);
            this.Logger.Info(String.Format("Connection to {0}:{1} closed. Local end point was {2}:{3}",
                                            conn.RemoteAddress, conn.RemotePort, conn.LocalAddress, conn.LocalPort));
            this.activeConnections.Remove(conn);
        }

        /// <summary>
        /// A client has sent a new message.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessMessage(object sender, MessageReceivedEventArgs e)
        {
            IConnection conn = sender as IConnection;
            if (e.Message.Header.Command.Equals(MessageTypes.PEERLIST_REQUEST))
            {
                var peerRequestList = (PeerListRequest)Serialisation.Deserialize(e.Message.Payload, typeof(PeerListRequest));
                var handler = new ClientHandler(conn);
                var peerListRequestEventArgs = new PeerListRequestEventArgs(handler, peerRequestList.PeerCount,peerRequestList.ListenPort);
                this.PeerListRequestReceived.Invoke(this, peerListRequestEventArgs);
            }
            else if (e.Message.Header.Command.Equals(MessageTypes.PING))
            {
                var ping = (Ping)Serialisation.Deserialize(e.Message.Payload, typeof(Ping));
                var handler = new ClientHandler(conn);
                var clientEventArgs = new ClientEventArgs(handler,ping.ListenPort);
                this.PingReceived.Invoke(this, clientEventArgs);
            }
            else if (e.Message.Header.Command.Equals(MessageTypes.OFFLINE))
            {
                var offline = (Offline)Serialisation.Deserialize(e.Message.Payload, typeof(Offline));
                var handler = new ClientHandler(conn);
                var clientEventArgs = new ClientEventArgs(handler,offline.ListenPort);
                this.PeerIsOffline.Invoke(this, clientEventArgs);
            }
            else
            {
                this.Logger.Warn("Received message with invalid type: " + e.Message.Header.Command);
            }
        }


        #region Properties and private member

        /// <summary>
        /// IP address, that the server is listening on.
        /// </summary>
        public IPAddress IP { get; private set; }

        /// <summary>
        /// Port, that the server is listening on.
        /// </summary>
        public int Port { get; private set; }

        /// <summary>
        /// Sending and receiving messages will timeout after the specified time in seconds.
        /// </summary>
        public int Timeout { get; set; }

        public uint PeerTimeout { get; set; }

        /// <summary>
        /// The maximum number of parallel connections. Must be a value between 1 and 1000.
        /// </summary>
        public uint AllowedClients
        {
            get { return this.backlog; }
            set 
            {
                if (value > 0 && value <= 1000)
                {
                    this.backlog = value;
                }
            }
        }

        /// <summary>
        /// Gets the logger.
        /// </summary>
        public ILogger Logger { get; private set; }

        private IConnection listenConnection;

        private uint backlog;

        private ThreadSafeList<IConnection> activeConnections;

        #endregion


        #region Events
     
        /// <summary>
        /// A peer wants to connect to the network and requests a list of already connected peers.
        /// </summary>
        public event EventHandler<PeerListRequestEventArgs> PeerListRequestReceived;

        /// <summary>
        /// A ping was received, signaling that the peer is still online. 
        /// </summary>
        public event EventHandler<ClientEventArgs> PingReceived;

        /// <summary>
        /// A peer is going offline now and informed the server to remove him from the list.
        /// </summary>
        public event EventHandler<ClientEventArgs> PeerIsOffline;

        #endregion

    }
}
